using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Telerik.Web.Mvc;
using TelerikMvcGridCustomBindingHelper.Mapper;
using TelerikMvcGridCustomBindingHelper.Util;

namespace TelerikMvcGridCustomBindingHelper.DynamicQuery
{
    class ExpressionWhereClause<TEntity, TViewModel> : BaseDynamicWhereClause<TEntity, TViewModel, Expression<Func<TEntity, bool>>>
        where TEntity : class
        where TViewModel : class
    {
        public new Expression<Func<TEntity, bool>> Predicate { get { return RebindParameters(base.Predicate); } }

        private static Expression<Func<TEntity, bool>> RebindParameters(Expression<Func<TEntity, bool>> predicate)
        {
            var expression = ParameterRebinder.ReplaceParameters(predicate.Parameters.ToDictionary(p => p), predicate.Body, true);
            return Expression.Lambda<Func<TEntity, bool>>(expression, predicate.Parameters[0]);
        }

        public ExpressionWhereClause(FilterDescriptor filterDescriptor, CaseInsensitive<TEntity> caseInsensitive, bool queryAcceptNullValuesWhenFiltering, ValueResolvers queryValueResolvers)
            : base(filterDescriptor, caseInsensitive, queryAcceptNullValuesWhenFiltering, queryValueResolvers) { }

        public ExpressionWhereClause() { }

        public void OrElse(ExpressionWhereClause<TEntity, TViewModel> expressionWhereClause)
        {
            MergePredicateFrom(expressionWhereClause, true);
        }

        public void AndAlso(ExpressionWhereClause<TEntity, TViewModel> expressionWhereClause)
        {
            MergePredicateFrom(expressionWhereClause, false);
        }

        protected override Expression<Func<TEntity, bool>> OrElse(Expression<Func<TEntity, bool>> left, Expression<Func<TEntity, bool>> right)
        {
            if (left == null)
                return right;

            if (right == null)
                return left;

            return left.OrElse(right);
        }

        protected override Expression<Func<TEntity, bool>> AndAlso(Expression<Func<TEntity, bool>> left, Expression<Func<TEntity, bool>> right)
        {
            if (left == null)
                return right;

            if (right == null)
                return left;

            return left.AndAlso(right);
        }

        protected override Expression<Func<TEntity, bool>> BuildPredicateWithBooleanSubstitutes(IEnumerable<object> booleanSubstitutes)
        {
            Guard.If((ConvertedValue is bool || ConvertedValue == null) == false)
                .Throw<ArgumentException>(string.Format("Property \"{0}.{1}\" was not of type boolean.", typeof(TEntity).Name, TargetProperty));

            switch (InnerFilterDescriptor.Operator)
            {
                case FilterOperator.IsEqualTo:
                    return OrElse(base.Predicate, GetBooleanSubstitutePredicate(booleanSubstitutes));

                case FilterOperator.IsNotEqualTo:
                    return OrElse(base.Predicate, GetBooleanSubstitutePredicate(booleanSubstitutes, true));

                default:
                    throw new NotSupportedException(string.Format("The method or expression \"{0}\" is not supported when the value is a boolean.", InnerFilterDescriptor.Operator));
            }
        }

        private Expression<Func<TEntity, bool>> GetBooleanSubstitutePredicate(IEnumerable<object> substitutes, bool negate = false)
        {
            Expression<Func<TEntity, bool>> predicate = null;

            foreach (var substitute in substitutes.Reverse())
            {
                var propertyExpression = GetGridPropertyExpression();

                Expression booleanExp;

                if ((GridPropertyInfo.PropertyType == typeof(bool?)) && substitute != null)
                {
                    booleanExp = Expression.Equal(propertyExpression.Body, Expression.Convert(Expression.Constant(substitute), typeof(bool?)));
                }
                else
                {
                    booleanExp = Expression.Equal(propertyExpression.Body, Expression.Constant(substitute));
                }

                if (negate)
                {
                    booleanExp = Expression.Not(booleanExp);
                }

                var lambda = Expression.Lambda<Func<TEntity, bool>>(booleanExp, propertyExpression.Parameters);

                predicate = OrElse(predicate, lambda);
            }

            return predicate;
        }

        protected override Expression<Func<TEntity, bool>> BuildPredicateCaseSensitive(object value)
        {
            Expression exp;
            var filterOperator = InnerFilterDescriptor.Operator;

            if (value == null)
            {
                filterOperator = EnforceEqualityOperator();
            }

            var expression = GetGridPropertyExpression();

            var constant = Expression.Constant(value, GridPropertyInfo.PropertyType);
            switch (filterOperator)
            {
                case FilterOperator.IsEqualTo:
                    exp = Expression.Equal(expression.Body, constant);
                    break;

                case FilterOperator.IsNotEqualTo:
                    exp = Expression.NotEqual(expression.Body, constant);
                    break;

                case FilterOperator.StartsWith:
                    exp = Expression.Call(expression.Body, "StartsWith", null, constant);
                    break;

                case FilterOperator.EndsWith:
                    exp = Expression.Call(expression.Body, "EndsWith", null, constant);
                    break;

                case FilterOperator.Contains:
                    exp = Expression.Call(expression.Body, "Contains", null, constant);
                    break;

                case FilterOperator.DoesNotContain:
                    exp = Expression.Not(Expression.Call(expression.Body, "Contains", null, constant));
                    break;

                case FilterOperator.IsGreaterThan:
                    exp = Expression.GreaterThan(expression.Body, constant);
                    break;

                case FilterOperator.IsGreaterThanOrEqualTo:
                    exp = Expression.GreaterThanOrEqual(expression.Body, constant);
                    break;

                case FilterOperator.IsLessThan:
                    exp = Expression.LessThan(expression.Body, constant);
                    break;

                case FilterOperator.IsLessThanOrEqualTo:
                    exp = Expression.LessThanOrEqual(expression.Body, constant);
                    break;

                // I dont kown how to implement this
                //case FilterOperator.IsContainedIn:

                default:
                    throw new NotSupportedException(string.Format("The method or expression \"{0}\" is not supported.", InnerFilterDescriptor.Operator));
            }

            return Expression.Lambda<Func<TEntity, bool>>(exp, expression.Parameters);
        }

        private LambdaExpression GetGridPropertyExpression()
        {
            var customExpression = GridPropertyInfo.CustomExpression ?? GridPropertyInfo.EntityProperty;
            return customExpression ?? Reflector.GetPropertyExpression<TEntity>(GridPropertyInfo.PropertyPath, GridPropertyInfo.PropertyType);
        }

        protected override Expression<Func<TEntity, bool>> BuildPredicateCaseInsensitive(string value)
        {
            Expression exp = GetGridPropertyExpression();
            var filterOperator = InnerFilterDescriptor.Operator;

            if (value == null)
            {
                filterOperator = EnforceEqualityOperator();
            }

            var expression = Expression.Lambda(Expression.Call(((LambdaExpression)exp).Body, "ToUpper", null), ((LambdaExpression)exp).Parameters);

            var constant = Expression.Constant(value, GridPropertyInfo.PropertyType);
            switch (filterOperator)
            {
                case FilterOperator.IsEqualTo:
                    exp = Expression.Equal(expression.Body, constant);
                    break;

                case FilterOperator.IsNotEqualTo:
                    exp = Expression.NotEqual(expression.Body, constant);
                    break;

                case FilterOperator.StartsWith:
                    exp = Expression.Call(expression.Body, "StartsWith", null, constant);
                    break;

                case FilterOperator.EndsWith:
                    exp = Expression.Call(expression.Body, "EndsWith", null, constant);
                    break;

                case FilterOperator.Contains:
                    exp = Expression.Call(expression.Body, "Contains", null, constant);
                    break;

                case FilterOperator.DoesNotContain:
                    exp = Expression.Not(Expression.Call(expression.Body, "Contains", null, constant));
                    break;

                // I dont kown how to implement this
                //case FilterOperator.IsContainedIn:

                default:
                    throw new NotSupportedException(string.Format("The method or expression \"{0}\" is not supported.", InnerFilterDescriptor.Operator));
            }

            return Expression.Lambda<Func<TEntity, bool>>(exp, GetGridPropertyExpression().Parameters);
        }

        protected override Expression<Func<TEntity, bool>> BuildPredicateThatAcceptNullValues(INullSubstitute nullSubstitutes, bool useNotEquals)
        {
            if (nullSubstitutes != null)
            {
                var caseInsensitive = false;

                IList<object> nullValues = new List<object>(nullSubstitutes.NullValues().Count);

                foreach (var nullValue in nullSubstitutes.NullValues())
                {
                    var changeType = Reflector.TryChangeType(nullValue, GridPropertyInfo.PropertyType);
                    if (GridPropertyInfo.PropertyType == typeof(string) && changeType != null && nullSubstitutes.IsCaseInsensitive)
                    {
                        changeType = ((string)changeType).ToUpper(GridModelMapper.ConfigurationObject.DefaultCulture);
                        caseInsensitive = true;
                    }
                    if (nullValues.Any(o => Equals(o, changeType)) == false)
                    {
                        nullValues.Add(changeType);
                    }
                }

                Expression<Func<TEntity, bool>> predicate = null;

                foreach (var nullValue in nullValues)
                {
                    if (nullValue != null)
                    {
                        predicate = BuildPredicateThatAcceptNullValues(predicate, useNotEquals, nullValue, caseInsensitive);
                    }
                    else
                    {
                        predicate = BuildPredicateThatAcceptNullValues(predicate, useNotEquals, null);
                    }
                }

                return OrElse(base.Predicate, predicate);
            }

            return BuildPredicateThatAcceptNullValues(base.Predicate, useNotEquals, null);
        }

        private Expression<Func<TEntity, bool>> BuildPredicateThatAcceptNullValues(Expression<Func<TEntity, bool>> predicate, bool useNotEquals, object value, bool caseInsensitive = false)
        {
            if (value == null && GridPropertyInfo.PropertyType.IsValueType)
            {
                if (Nullable.GetUnderlyingType(GridPropertyInfo.PropertyType) == null)
                {
                    return null;
                }
            }

            var exp = GetGridPropertyExpression();

            if (caseInsensitive && value is string)
            {
                value = ((string)value).ToUpper(GridModelMapper.ConfigurationObject.DefaultCulture);
                exp = Expression.Lambda(Expression.Call(exp.Body, "ToUpper", null));
            }

            var constant = Expression.Constant(value, GridPropertyInfo.PropertyType);
            Expression equality = Expression.Equal(exp.Body, constant);

            if (useNotEquals)
            {
                equality = Expression.Not(equality);
            }

            var pred = Expression.Lambda<Func<TEntity, bool>>(equality, GetGridPropertyExpression().Parameters);

            return OrElse(predicate, pred);
        }
    }
}